﻿using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Reflection;
using ServiceOperater.Models;

namespace MvvmFoundation.Wpf
{

    /// <summary>
    /// This class is a complete and reusable view-model for a single underlying
    /// model instance.
    /// </summary>
    /// <remarks>
    /// <para>
    /// By using the new .NET 4 dynamic capabilities this class provides automatic
    /// "delegation" properties for the wrapped model instance; if a setter is invoked
    /// this class will raise the appropriate property changed event.
    /// The class also supports the <c>[AffectsOtherProperty]</c> attribute defined
    /// in the model.
    /// </para>
    /// Because this class can act as a valuable view-model without being extended
    /// it is not implemented as an abstract class.
    /// </remarks>
    /// <see cref="MvvmDemo.Utilities.AffectsOtherPropertyAttribute"/>
    public class DynamicViewModel : DynamicObject, INotifyPropertyChanged, IDisposable, IDynamicMetaObjectProvider
    {

        /// <summary>
        /// Creates a new view-model instance that encapsulates the given model instance.
        /// </summary>
        /// <param name="model">The non-null model instance to encapsulate</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Contracts", "Requires")]
        public DynamicViewModel(DomainObject model)
        {
            Contract.Requires(model != null, "Cannot encapsulate a null model");
            this.ModelInstance = model;

            // Does this model raise property changed events? //如果Model实现实现了INotifyPropertyChanged接口
            if (model is INotifyPropertyChanged)
            {
                this.ModelRaisesPropertyChangedEvents = true;

                var raisesPropChangedEvents = model as INotifyPropertyChanged;
                raisesPropChangedEvents.PropertyChanged += this.OnModelPropertyChanged; //将Model的PropertyChanged和VM的PropertyChanged处理函数进行关联,,,即如果Model实现了INotifyPropertyChanged,那么Model的PropertyChanged时,VM也会进行相应的处理,这样做的好处是,在View层看来,所关注的属性就像在VM上一样.
            }
        }

        /// <summary>
        /// Dispose of this instance.
        /// </summary>
        public void Dispose()
        {
            // If the underlying model instance raises property changed events, stop listening now
            if (this.ModelInstance != null && this.ModelRaisesPropertyChangedEvents)
            {
                var raisesPropChangedEvents = this.ModelInstance as INotifyPropertyChanged;
                raisesPropChangedEvents.PropertyChanged -= this.OnModelPropertyChanged;
            }//if

            // We don't need to keep pointing at the model instance
            this.ModelInstance = null;
        }


        /// <summary>
        /// The underlying model instance that this view-model instance is encapsulating
        /// </summary>
        internal DomainObject ModelInstance { get; private set; }


        /// <summary>
        /// Indicates if the model instance raises property changed events
        /// </summary>
        protected bool ModelRaisesPropertyChangedEvents { get; private set; }



        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
        #region INotifyPropertyChanged Members
        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>
        /// Raised when a property on this object has a new value.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;


        /// <summary>
        /// Indicates if the <c>VerifyPropertyName</c> and <c>OnPropertyChanged</c>
        /// methods should raise an exception if the property name doesn't exist.
        /// </summary>
        /// <remarks>
        /// When <c>true</c>, the <c>VerifyPropertyName</c> and <c>OnPropertyChanged</c>
        /// methods will throw an exception if the supplied property name doesn't exist.
        /// If this property is <c>false</c>, then the two methods will instead
        /// print a message with <c>Debug.Fail()</c>
        /// </remarks>
        /// <value>
        /// The default value is <c>false</c>, so the <c>Debug.Fail()</c> will be
        /// used by default.
        /// </value>
        protected virtual bool ThrowOnInvalidPropertyName { get; private set; }


        /// <summary>
        /// Raises this object's PropertyChanged event.
        /// </summary>
        /// <param name="propertyName">The property that has a new value.</param>
        protected virtual void RaisePropertyChanged(string propertyName)
        {
            Contract.Requires(string.IsNullOrWhiteSpace(propertyName) == false, "The property name must not be null or empty");
            if (this.ModelInstance == null)
                throw new InvalidOperationException("This view-model instance has already been disposed; it no longer supports 'get' opertions");
            this.VerifyPropertyName(propertyName);

            // Is anybody out there?
            PropertyChangedEventHandler handler = this.PropertyChanged;
            if (handler == null) return;

            // Somebody is listening, so raise a property changed event
            var args = new PropertyChangedEventArgs(propertyName);
            handler(this, args);
        }


        /// <summary>
        /// One of the nuances of WPF data-binding is that they will fail silently
        /// </summary>
        /// <remarks>
        /// With the dynamic proxy properties this method must check both the model and the view-model classes
        /// before deciding if a property name is spelled incorrectly.
        /// </remarks>
        [DebuggerStepThrough]
        [Conditional("DEBUG")]
        protected void VerifyPropertyName(string propertyName)
        {
            Contract.Requires(this.ModelInstance != null, "This view-model instance has already been disposed");

            // If the property exists on the model then there's nothing to do
            if (TypeDescriptor.GetProperties(this.ModelInstance)[propertyName] != null) return;

            // Or if the property exists on the view-model there's nothing to do
            if (TypeDescriptor.GetProperties(this)[propertyName] != null) return;

            // The property didn't exist, we're going to raise an error of some kind
            string mesage = "Invalid property name: " + propertyName;
            if (this.ThrowOnInvalidPropertyName)
                throw new Exception(mesage);
            else
                Debug.Fail(mesage);
        }


        /// <summary>
        /// Called when the underlying model instance raises the property changed event.
        /// </summary>
        /// <remarks>
        /// This method re-raises the property changed event as through it was comming from this view-model
        /// instance.  This may cause the view layer (XAML data bindings) to update.
        /// </remarks>
        /// <param name="sender">Should be the underlying model instance</param>
        /// <param name="args">Information about the property that changed</param>
        private void OnModelPropertyChanged(object sender, PropertyChangedEventArgs args)
        {
            Contract.Requires(sender != null, "The sender of this event should never be null; there is likely a bug in the model class:" + this.ModelInstance.GetType());
            Contract.Requires(args != null, "The property changed arguments should never be null; there is a but in the model class:" + this.ModelInstance.GetType());
            Contract.Requires(string.IsNullOrWhiteSpace(args.PropertyName) == false);
            Contract.Requires(sender == this.ModelInstance, "The sender was incorrect, there is a bug");

            this.RaisePropertyChanged(args.PropertyName);
        }

        #endregion



        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
        #region Dynamic capabilities
        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>
        /// This method is called when a property is accessed that is not actually defined on this class.
        /// (Note that if the property is actually defined on the view-model class then it is called directly and no automatic
        /// delegation to the model occurs).
        /// </summary>
        /// <param name="binder">
        /// Provides information about the object that called the dynamic operation.
        /// The <c>binder.Name</c> property provides the name of the member on which the dynamic operation is performed.
        /// </param>
        /// <param name="result">
        /// The result of the get operation.
        /// In this case, this will be the result of accessing the property with the name <c>binder.Name</c> on the model instance.
        /// </param>
        /// <returns>
        /// <c>true</c> if the underlying model instance has a property named <c>binder.Name</c> that is readable;
        /// returns <c>false</c> if the underlying model instance does NOT have such a property, or it is not readable.
        /// </returns>
        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            if (this.ModelInstance == null)
                throw new InvalidOperationException("This view-model instance has already been disposed; it no longer supports 'get' operations");

            // A caller is trying to access a property named binder.Name on the view-model.
            // No such statically defined property exists (or it would have been called rather than this method)
            // so this method will automatically delegate the "set property" to the underlying model instance
            string propertyName = binder.Name;
            Contract.Assume(propertyName != null, "The binder property name should never be null");
            PropertyInfo property = this.ModelInstance.GetType().GetProperty(propertyName);
            if (property == null || property.CanRead == false)
            {
                result = null;
                return false;
            }//if - no such property on the model

            // Return the value of the underlying model property
            result = property.GetValue(this.ModelInstance, null);
            return true;
        }


        /// <summary>
        /// This method is called when a property is written that is not actually defined on this class.
        /// (Note that if the property is actually defined on the view-model class then it is called directly and no automatic
        /// delegation to the model occurs).
        /// </summary>
        /// <param name="binder">
        /// Provides information about the object that called the dynamic operation.
        /// The <c>binder.Name</c> property provides the name of the member on which the dynamic operation is performed.
        /// </param>
        /// <param name="value">
        /// The new property value to set.
        /// </param>
        /// <returns>
        /// <c>true</c> if the underlying model instance has a property named <c>binder.Name</c> that is writable;
        /// <c>false</c> if the underlying model instance does NOT have such a property, or it is not writable.
        /// </returns>
        public override bool TrySetMember(SetMemberBinder binder, object value)
        {
            if (this.ModelInstance == null)
                throw new InvalidOperationException("This view-model instance has already been disposed; it no longer supports 'get' operations");

            // Somebody is trying to set a property named binder.Name on the view-model.
            // No such statically defined property exists (or it would have been called rather than this method)
            // so this method will automatically delegate the "set property" to the underlying model instance
            string propertyName = binder.Name;
            Contract.Assume(string.IsNullOrWhiteSpace(propertyName) == false, "The binder property name should never be ");
            PropertyInfo property = this.ModelInstance.GetType().GetProperty(propertyName);
            if (property == null || property.CanWrite == false)
                return false;

            // Set the value of the underlying model property
            property.SetValue(this.ModelInstance, value, null);

            // Execute the common setter functionality on the property
            // This includes raising events and processing other affected properties
            this.CommonSetterFunctionality(property);

            return true;
        }


        /// <summary>
        /// This method encapsulates all the logic that must be done when a property
        /// on the underlying model is set.
        /// </summary>
        /// <param name="property">The property on the model that was set</param>
        /// <remarks>
        /// This method delegates the hard work to <see cref="CommonSetterFunctionality(PropertyInfo)"/>
        /// </remarks>
        protected void CommonSetterFunctionality(string propertyName)
        {
            Contract.Requires(string.IsNullOrWhiteSpace(propertyName) == false, "The property name must not be null or whitespace");
            PropertyInfo property = this.ModelInstance.GetType().GetProperty(propertyName);
            Contract.Assert(property != null, "There is no such property on the underlying model");
            this.CommonSetterFunctionality(property);
        }


        /// <summary>
        /// This method encapsulates all the logic that must be done when a property
        /// on the underlying model is set.
        /// </summary>
        /// <param name="property">The property on the model that was set</param>
        protected virtual void CommonSetterFunctionality(PropertyInfo property)
        {
            // Raise the property changed event IF AND ONLY IF the underlying model class isn't already doing this
            // (we don't want to raise the property changed event twice for the same property change)
            if (this.ModelRaisesPropertyChangedEvents == false)
            {
                this.RaisePropertyChanged(property.Name);

                // Does the given property affect any other properties?
                // If so, raise the property changed event for those properties as well
                var affectsOtherProperties = property.GetCustomAttributes(typeof(AffectsOtherPropertyAttribute), true);
                foreach (AffectsOtherPropertyAttribute otherPropertyAttr in affectsOtherProperties)
                {
                    this.RaisePropertyChanged(otherPropertyAttr.AffectsProperty);
                }
            }//if - model doesn't raise property changed events
        }

        #endregion

    }//class

}//namespace
